namespace System;
/// <summary>
/// DynamicInvoker
/// </summary>
public static class DynamicInvoker
{
    /// <summary>
    ///
    /// </summary>
    private const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    /// <summary>
    ///
    /// </summary>
    private static volatile Dictionary<int, DynamicMethodExecutor?> _cachedMembers = new Dictionary<int, DynamicMethodExecutor?>();
    /// <summary>
    ///
    /// </summary>
    private static readonly object lockState = new object();
    /// <summary>
    ///
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <param name="methodname"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static object? Invoke<T>(
        this T obj,
        string methodname,
        params object[] args)
    {
        var type = (obj ?? throw new ArgumentNullException(nameof(obj))).GetType();

        var hash = Hash(type, methodname, args);

        var exists = _cachedMembers.TryGetValue(hash, out var method);
        if (exists) return method?.Execute(obj, args);

        lock (lockState)
        {
            exists = _cachedMembers.TryGetValue(hash, out method);
            if (exists) return method?.Execute(obj, args);
            var argtypes = GetArgTypes(args);
            var m = GetMember(type, methodname, argtypes);
            method = m is null ? null : DynamicMethodExecutor.Compiled(m);
            var dict = new Dictionary<int, DynamicMethodExecutor?>(_cachedMembers) { { hash, method } };
            _cachedMembers = dict;
            return method?.Execute(obj, args);
        }
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="type"></param>
    /// <param name="methodname"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    private static int Hash(
        Type type,
        string methodname,
        object[] args)
    {
        var hash = 23;
        hash = hash * 31 + type.GetHashCode();
        hash = hash * 31 + methodname.GetHashCode();
        foreach (var t in args)
        {
            var argtype = t.GetType();
            hash = hash * 31 + argtype.GetHashCode();
        }
        return hash;
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="args"></param>
    /// <returns></returns>
    private static Type[] GetArgTypes(object[] args)
    {
        var argtypes = new Type[args.Length];

        for (var i = 0; i < args.Length; i++)
        {
            var argtype = args[i].GetType();
            argtypes[i] = argtype;
        }

        return argtypes;
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="type"></param>
    /// <param name="name"></param>
    /// <param name="argtypes"></param>
    /// <returns></returns>
    private static MethodInfo? GetMember(
        Type type,
        string name,
        Type[] argtypes)
    {
        while (true)
        {
            var methods = type.GetMethods(bindingFlags).Where(m => m.Name == name).ToArray();

            var member = methods.FirstOrDefault(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(argtypes)) ??
                         methods.FirstOrDefault(m => m.GetParameters().Select(p => p.ParameterType).ToArray().Matches(argtypes));
            if (member is not null)
            {
                return member;
            }

            var t = type.GetTypeInfo().BaseType;
            if (t is null)
            {
                return null;
            }

            type = t;
        }
    }
    /// <summary>
    ///
    /// </summary>
    /// <param name="arr"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    private static bool Matches(this Type[] arr, Type[] args)
    {
        if (arr.Length != args.Length) return false;
        for (var i = 0; i < args.Length; i++)
        {
            if (!arr[i].IsAssignableFrom(args[i]))
                return false;
        }
        return true;
    }

}